﻿using System;
using System.IO;
using System.Security.Cryptography;

namespace NTQOnlineExam.Services.Infrastructure.Encryption
{
    public class SimpleAESEncryptionService : IEncryptionService
    {
        private readonly byte[] _key = { 122, 216, 18, 11, 23, 26, 85, 45, 114, 182, 127, 162, 37, 112, 222, 208, 241, 24, 174, 144, 173, 52, 196, 29, 24, 26, 15, 215, 131, 236, 52, 209 };
        private readonly byte[] _vector = { 147, 65, 192, 111, 24, 3, 113, 119, 231, 121, 221, 112, 79, 33, 115, 157 };
        private const string EncryptionKey = "BF990109ABEC4C8B97BDDA9C8730C894";
        private const string EncryptionSalt = "9v7aA3DZC7BA4d8L767jjaksj72epoiq";
        private readonly ICryptoTransform _encryptorTransform;
        private readonly ICryptoTransform _decryptorTransform;
        private readonly System.Text.UTF8Encoding _utfEncoder;

        public SimpleAESEncryptionService()
        {
            //This is our encryption method
            RijndaelManaged rm = new RijndaelManaged();

            //Create an encryptor and a decryptor using our encryption method, key, and vector.
            _encryptorTransform = rm.CreateEncryptor(_key, _vector);
            _decryptorTransform = rm.CreateDecryptor(_key, _vector);

            //Used to translate bytes to text and vice versa
            _utfEncoder = new System.Text.UTF8Encoding();
        }


        /// <summary>
        /// Encrypt some text and return a string suitable for passing in a URL.
        /// </summary>
        /// <param name="text"></param>
        /// <returns></returns>
        public string EncryptToString(string text)
        {
            return ByteArrayToString(Encrypt(text));
        }

        /// <summary>
        /// Encrypt some text and return an encrypted byte array.
        /// </summary>
        /// <param name="text"></param>
        /// <returns></returns>
        public byte[] Encrypt(string text)
        {
            //Translates our text value into a byte array.
            Byte[] bytes = _utfEncoder.GetBytes(text);

            //Used to stream the data in and out of the CryptoStream.
            MemoryStream memoryStream = new MemoryStream();

            /*
             * We will have to write the unencrypted bytes to the stream,
             * then read the encrypted result back from the stream.
             */
            #region Write the decrypted value to the encryption stream

            CryptoStream cs = new CryptoStream(memoryStream, _encryptorTransform, CryptoStreamMode.Write);
            cs.Write(bytes, 0, bytes.Length);
            cs.FlushFinalBlock();

            #endregion

            #region Read encrypted value back out of the stream

            memoryStream.Position = 0;
            byte[] encrypted = new byte[memoryStream.Length];
            memoryStream.Read(encrypted, 0, encrypted.Length);

            #endregion

            //Clean up.
            cs.Close();
            memoryStream.Close();

            return encrypted;
        }

        /// <summary>
        /// Decryption when working with string
        /// </summary>
        /// <param name="encryptedString"></param>
        /// <returns></returns>
        public string DecryptString(string encryptedString)
        {
            return Decrypt(StringToByteArray(encryptedString));
        }

        /// <summary>
        /// Decryption when working with byte arrays
        /// </summary>
        /// <param name="encryptedValue"></param>
        /// <returns></returns>
        public string Decrypt(byte[] encryptedValue)
        {
            #region Write the encrypted value to the decryption stream

            MemoryStream encryptedStream = new MemoryStream();
            CryptoStream decryptStream = new CryptoStream(encryptedStream, _decryptorTransform, CryptoStreamMode.Write);
            decryptStream.Write(encryptedValue, 0, encryptedValue.Length);
            decryptStream.FlushFinalBlock();

            #endregion

            #region Read the decrypted value from the stream.

            encryptedStream.Position = 0;
            Byte[] decryptedBytes = new Byte[encryptedStream.Length];
            encryptedStream.Read(decryptedBytes, 0, decryptedBytes.Length);
            encryptedStream.Close();

            #endregion

            return _utfEncoder.GetString(decryptedBytes);
        }

        public string GenerateChecksum(string data)
        {
            return CryptographyUtilities.CreateSHAHash(data + EncryptionSalt);
        }

        public bool VerifyChecksum(string data, string checksum)
        {
            return CryptographyUtilities.CreateSHAHash(data + EncryptionSalt) == checksum;
        }

        public string GenerateExpirableChecksum(string data)
        {
            if (data == null) return null;
            var encodeValue = string.Format("{0}|{1}", data, DateTime.Now.Ticks);
            return EncryptToString(encodeValue);
        }

        public bool VerifyExpirableChecksum(string data, string checksum)
        {
            return VerifyExpirableChecksum(data, checksum, 30);
        }

        public bool VerifyExpirableChecksum(string data, string checksum, int expiredTimeInMinute)
        {
            if (data == null) return false;
            try
            {
                string decryptedData = DecryptString(checksum);
                var splitters = decryptedData.Split('|');
                if (splitters.Length == 2)
                {
                    long ticks;
                    if (long.TryParse(splitters[1], out ticks))
                    {
                        DateTime dateTime = new DateTime(ticks);
                        TimeSpan timeSpan = DateTime.Now - dateTime;
                        if (timeSpan.TotalMinutes >= 0 && timeSpan.TotalMinutes <= expiredTimeInMinute)
                        {
                            return data == splitters[0];
                        }
                    }
                }
            }
            catch
            {
            }
            return false;
        }

        /// <summary>
        /// Convert a string to a byte array.  NOTE: Normally we'd create a Byte Array from a string using an ASCII encoding (like so).
        ///      System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
        ///      return encoding.GetBytes(str);
        /// However, this results in character values that cannot be passed in a URL.  So, instead, I just
        /// layout all of the byte values in a long string of numbers (three per - must pad numbers less than 100).
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        private static byte[] StringToByteArray(string str)
        {
            if (str.Length == 0)
            {
                throw new Exception("Invalid string value in StringToByteArray");
            }

            byte val;
            byte[] byteArr = new byte[str.Length / 3];
            int i = 0;
            int j = 0;
            do
            {
                val = byte.Parse(str.Substring(i, 3));
                byteArr[j++] = val;
                i += 3;
            }
            while (i < str.Length);
            return byteArr;
        }

        /// <summary>
        /// Same comment as above.  Normally the conversion would use an ASCII encoding in the other direction:
        ///      System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding();
        ///      return enc.GetString(byteArr);    
        /// </summary>
        /// <param name="byteArr"></param>
        /// <returns></returns>
        private static string ByteArrayToString(byte[] byteArr)
        {
            byte val;
            string tempStr = "";
            for (int i = 0; i <= byteArr.GetUpperBound(0); i++)
            {
                val = byteArr[i];
                if (val < (byte)10)
                    tempStr += "00" + val.ToString();
                else if (val < (byte)100)
                    tempStr += "0" + val.ToString();
                else
                    tempStr += val.ToString();
            }
            return tempStr;
        }
    }
}
